package Commons;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;

/**Questa classe implementa il Client che trasferisce il file ad un ServerTrasferimentoFileUDP
 *
 * @author Team5
 */
public class ClientTrasferimentoFileUDP {
         int bufferDatagramSize;
         byte buffDatagram[];

         int bufferFileSize ;
         byte buffFile [];

         int bufferContatoreSize ;
         byte buffContatore [] ;

         InetAddress addressServer;
         int portServer;

         String link;
         String nuovoNomeFile;

    /**Costruttore con parametri in ingresso relativi alle informazioni necessario per trasmettere il file.
     *
     * @param addressServer l'indirizzo del Server su cui si intende effettuare il trasferimento
     * @param portServer la porta del servizio del Server
     * @param link il percorso relativo del file comprensivo di nome
     * @param nuovoNomeFile il nome che si intende assegnare al file quando sarà memorizzato sul Server

     */

    public ClientTrasferimentoFileUDP(InetAddress addressServer, int portServer, String link, String nuovoNomeFile) {
        this.addressServer = addressServer;
        this.portServer = portServer;
        this.link = link;
        this.nuovoNomeFile = nuovoNomeFile;
        this.bufferDatagramSize = 0;
        this.bufferFileSize = 1024;
        this.bufferContatoreSize = 0;

    }

    
    /**
     * Questo metodo permette la customizzazione dei buffers
     * 
     * @param bufferContatoreSize la grandezza relativa al buffer adoperato per il numero di sequenza dei pacchetti
     * @param bufferFileSize la grandezza relativa al buffer adoperato per la lettura del file
     */

    public void configuraBuffers(int bufferContatoreSize,int bufferFileSize)
    {
        this.bufferFileSize = bufferFileSize;
        this.bufferContatoreSize = bufferContatoreSize;
        this.bufferDatagramSize = bufferFileSize+bufferContatoreSize;

        buffContatore  = new byte[bufferContatoreSize];
        buffFile = new byte[bufferFileSize];
        buffDatagram = new byte[bufferDatagramSize];
    }

    /**Questo metodo esegue l'invio del file al Server
     *
     * @return valore contentene l'esito del trasferimento
     * @throws SocketException
     * @throws IOException
     * @throws FileNotFoundException
     * @throws InterruptedException
     */
    public int invioFileRemotoUDP() throws SocketException, IOException, FileNotFoundException, InterruptedException
    {
          System.out.println("\nCollegamento al server: "+ addressServer+", porta: "+ portServer);
          System.out.println("\nRichiesta la porta per il trasferime del file.");
          int portaFile = richiestaPorta();
          System.out.println("\nPorta su cui trasferire il file:" + portaFile);
          int check = trasferimentoFile(portaFile);
          return check;

    }
    /**
     * Il metodo ritorna il numero di pacchetti in cui sarà suddiviso il file
     * @return il numero di pacchetti
     */

    private Integer numeroDiPacchetti()
    {
      File file =  new File(link);
      long fileSize = file.length();
      int numeroDiPacchetti = (int)(fileSize%bufferFileSize==0 ? fileSize/bufferFileSize:fileSize/bufferFileSize+1);
      return numeroDiPacchetti;
    }
    /**
     * Il metodo calcola la dimensione del padding da aggiungere all'ultimo Datagram
     *
     * @return la dimensione del padding in byte
     */
    private Integer dimensionePadding()
    {
      File file =  new File(link);
      long fileSize = file.length();
      int numeroPacchetti = numeroDiPacchetti();
      return (int)(((numeroPacchetti)*bufferFileSize)-fileSize);
    }

    /** Questa funzione richiede al Server l'assegnazione di una porta
     * dedicata al trasferimento del file
     * mediante l'invio di un apposito datagram
     *
     * @return il numero di porta dedicata al trasferimento del file
     * @throws SocketException
     * @throws IOException
     */
    private int richiestaPorta() throws SocketException, IOException {

            //Creazione di un socket per l'invio della richiesta
            DatagramSocket clientSocket = new DatagramSocket();

            //Definizione dei buffer per la costruzione del datagram
             byte[] sendData = new byte[1024];
             byte[] receiveData = new byte[1024];

             //Creazione della stringa di comando da cui il Server potrà ricavare le informazioni
             //necessarie alla configurazione del servizio di ricezione del file
             String comando = "SendFile:"+nuovoNomeFile+":"+numeroDiPacchetti()+":"+dimensionePadding();
             sendData = comando.getBytes();

             //Creazione e invio del datagram contenente la stringa di comando
             DatagramPacket packet = new DatagramPacket( sendData, sendData.length, addressServer,portServer );
             clientSocket.send (packet);

             //Attesa e ricezione della risposta del Server
             //contenente il numero della porta dedicata richiesta
             DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
             clientSocket.receive(receivePacket);

             //Decodifica del valore ricevuto e chiusura della connessione
             String snuovaPorta = new String(receivePacket.getData());
             Integer nuovaPorta = Integer.parseInt(snuovaPorta.trim());

             clientSocket.close();

             //Restituzione del valore di ritorno
             return nuovaPorta;
    }

    /**Questo metodo consente di configurare le dimensioni dei buffer utilizzati
     * per la lettura del file e la creazione dei datagram da trasferire,
     * minimizzando la dimensione del campo usato per i numeri di sequenza dei pacchetti inviati
     *
     */
    private void configuraBuffers()
    {

        this.bufferContatoreSize = numeroDiPacchetti().toString().getBytes().length;
        this.bufferDatagramSize = bufferFileSize+bufferContatoreSize;
        buffContatore  = new byte[bufferContatoreSize];
        buffFile = new byte[bufferFileSize];
        buffDatagram = new byte[bufferDatagramSize];

    }

    /**Questo metodo effettua il trasferimento del file sulla porta passata come parametro di ingresso
     *
     * @param portaFile il numero della porta del Server dedicata al trasferimento del file
     * @return il valore intero 1, se la funzione è stata eseguita interamente senza sollevamento di eccezioni
     * @throws SocketException
     * @throws FileNotFoundException
     * @throws IOException
     * @throws InterruptedException
     */
    private int trasferimentoFile(int portaFile) throws SocketException, FileNotFoundException, IOException, InterruptedException {

        //Configurazione ottimizzata dei buffer
        //qualora non effettuata diversamente in precedenza
        if(this.bufferDatagramSize==0 || this.bufferContatoreSize==0 )
        configuraBuffers();


      //Creazione della socket da cui inviare il file
      //e apertura del file
      DatagramSocket ds = new DatagramSocket();
      FileInputStream in = new FileInputStream(link);

      //Creazione di una variabile per la lettura della dimensione del file nel senguito del metodo
      File file =  new File(link);
      System.out.println("\nCLIENT: Trasferimento del File:" + link +". Size: "+ file.length()/1024+" KB.");
      int numeroDiPacchetti = numeroDiPacchetti();
      Integer i;
      System.out.println("BF: "+bufferFileSize+" BD: "+bufferDatagramSize+"BC: "+bufferContatoreSize);

      //Creazione di una variabile di appoggio per conservare i pacchetti in cui viene suddiviso il file
      //e potere facilmente effettuare la ritrasmissione di quelli richiesti dal Server
      byte [][] payloadPacchetti = new byte [numeroDiPacchetti][bufferFileSize];

      //Ciclo di invio del file escluso
      for(i=0;i<numeroDiPacchetti-1;i++)
      {
        //Aggiunta del numero di pacchetto al datagram
            String cont = i.toString();
            System.arraycopy(cont.getBytes(), 0, buffDatagram, 0, cont.getBytes().length);
             //System.out.println(buffDatagram.length);
            in.read(buffDatagram,bufferContatoreSize,bufferFileSize);
            //Copio nella variabile di appoggio il payload del datagram che sto per inviare
            System.arraycopy(buffDatagram, bufferContatoreSize, payloadPacchetti[i],0,bufferFileSize);
            ds.send(new DatagramPacket(buffDatagram,buffDatagram.length,addressServer,portaFile));
            //System.out.println("\nUpload : ["+i*buffer+"/"+file.length()+"] - ["+i*buffer*100/file.length()+"%]");
            System.out.println("Numero paccchetto: "+i);
            Thread.sleep(1);

      }

      System.out.println("CLIENT: Posizione dell'ultimo pacchetto da inviare: " + i);
      String cont = i.toString();
      System.arraycopy(cont.getBytes(), 0, buffDatagram, 0, cont.getBytes().length);
      //Calcolo del numero di byte dell'ultima parte di file da trasferire
      int size = (int)file.length()-((numeroDiPacchetti-1)*bufferFileSize);
      in.read(buffDatagram,bufferContatoreSize,size);
      //Creazione del buffer contenente il padding
      byte[] padding = new byte[dimensionePadding()];
      //Riempimento del buffer
      for (int j=0; j< padding.length; j++)
          //padding[j]=(new Integer("0")).byteValue();
          padding[j]=("0").getBytes()[0];
//      System.out.println("CLIENT: Dimensione del padding: " + dimensionePadding());
//      System.out.println("CLIENT: Contenuto del padding: " + (new String(padding)));
//      System.out.println("CLIENT: Dimensione del payload: " + size);
//      System.out.println("CLIENT: Dimensione del contatore: " + bufferContatoreSize);
//      System.out.println("CLIENT: Valore del contatore: " + (new String(cont.getBytes())));
//      System.out.println("CLIENT: Dimensione del pacchetto: " + buffDatagram.length);
//      System.out.println("CLIENT: Contenuto del payload: " + (new String(buffDatagram)));
//      System.out.println(padding);
      //Aggiunta del buffer padding al datagram
        System.arraycopy(padding, 0, buffDatagram, bufferContatoreSize+size, padding.length);
//      System.out.println("CLIENT: Contenuto del pacchetto: " + (new String(buffDatagram)));

      //Copio nella variabile di appoggio il payload del datagram che sto per inviare
      System.arraycopy(buffDatagram, bufferContatoreSize, payloadPacchetti[i],0,bufferFileSize);
      ds.send(new DatagramPacket(buffDatagram,buffDatagram.length,addressServer,portaFile));
      // System.out.println("\nUpload : ["+(numberPackets-1)*bufferFileSize+"/"+file.length()+"] - ["+(numberPackets-1)*buffer*100/file.length()+"%]");
      System.out.println("\nCLIENT: Trasferimento Completato.");
      in.close();


      //***********************************************************//
      //Implementazione ritrasmissione dei pacchetti non recapitati//
      //***********************************************************//

      //Creo una variabile per ricevere eventuali numeri di pacchetti da ristrasmettere
      byte[] pacchettoMancante = new byte[1024];

      //Mi metto in ricezione di eventuali richieste di ritrasmissione
      System.out.println("CLIENT: In attesa di eventuali richieste di ritrasmissione...");
      DatagramPacket richiestaRitrasmissione = new DatagramPacket(pacchettoMancante, pacchettoMancante.length);
      try
          {
          ds.receive(richiestaRitrasmissione);
          }
      catch (SocketTimeoutException timeout)
          {
          System.out.println("CLIENT: Raggiunto timeout durante l'attesa delle richieste di ritrasmissione");
          }

      //Creo una variabile in cui salvare il numero del pacchetto da ritrasmettere
      Integer numeroPacchetto = Integer.parseInt(new String(richiestaRitrasmissione.getData()).trim());

      //Ciclo di ritrasmissione:
      //ritrasmetto al server il pacchetto richiesto;
      //se la richiesta di ritrasmissione contiene un numero pari a quello dei pacchetti totali
      //assumo che il server abbia ricevuto tutti i pacchetti inoltrati
      //e chiudo la socket
      while(numeroPacchetto<numeroDiPacchetti)
          {
          System.out.println("CLIENT: Ricevuta richiesta ritrasmissione del pacchetto " + numeroPacchetto);
          cont = numeroPacchetto.toString();
          System.arraycopy(cont.getBytes(), 0, buffDatagram, 0, cont.getBytes().length);
          System.arraycopy(payloadPacchetti[numeroPacchetto], 0, buffDatagram, bufferContatoreSize,bufferFileSize);
          ds.send(new DatagramPacket(buffDatagram,buffDatagram.length,addressServer,portaFile));
          System.out.println("CLIENT: Pacchetto " + numeroPacchetto + " ritrasmesso");
          try
              {
              ds.receive(richiestaRitrasmissione);
              }
          catch (SocketTimeoutException timeout)
              {
              System.out.println("CLIENT: Raggiunto tiemout durante l'attesa delle richieste di ritrasmissione");
              }
              numeroPacchetto = Integer.parseInt(new String(richiestaRitrasmissione.getData()).trim());
          }
      System.out.println("CLIENT: Ricevuta conferma ricezione totale dei pacchetti");
      ds.close();

        return 1;
    }





}
